'use strict';

const path = require('path');
const fs = require('fs');

const pkgDir = require('pkg-dir');
const inquirer = require('inquirer');
const Conf = require('conf');
const archiver = require('archiver');
const ora = require('ora');
const fsExtra = require('fs-extra');
const colors = require('colors');
const {NodeSSH} = require('node-ssh');
const ssh = new NodeSSH();

const Command = require('@x3-cli-dev/command');
const log = require('@x3-cli-dev/log');
const {getProjectList} = require('@x3-cli-dev/request');
const {execCommandAsync, sleep, matchStrObj, WORKSPACE_CONFIG_NAME, WORKSPACE_SERVER_CONFIG_NAME} = require('@x3-cli-dev/utils');

class PublishCommand extends Command {
  defaultServer = {};

  init() {
    this.skipBuild = !!this._options.skipBuild;
    this.skipBackup = !!this._options.skipBackup;
    this.useServer = this._options.use;
    this.application = this._options.application;
    log.verbose('skipBuild', this.skipBuild);
    log.verbose('skipBackup', this.skipBackup);
    log.verbose('useServer', this.useServer);
    log.verbose('application', this.application);
  }

  async exec() {
    try {
      const projectInfo = await this.prepare();
      this.projectInfo = projectInfo;
      log.verbose('projectInfo', this.projectInfo);

      await this.execCommand();
    } catch (error) {
      log.error(error.message);
      if (process.env.LOG_LEVEL === 'verbose') {
        console.log(error);
      }
    } finally {
      process.exit(0);
    }
  }

  async prepare() {
    await this.checkConf();
    await this.checkSSH();
    return this.getProjectInfo();
  }

  async checkConf() {
    let cliPath = process.env.X3_CLI_HOME_PATH;
    let configName;
    // 是否使用本地工作空间配置
    if (fsExtra.pathExistsSync(path.join(process.cwd(), WORKSPACE_SERVER_CONFIG_NAME))) {
      cliPath = path.join(process.cwd());
      configName = '.x3server';
    }
    const config = new Conf({
      cwd: cliPath,
      configName,
    });
    let {server = {}, defaultServer} = config.get();
    log.verbose('config', config.get());
    if (!Object.keys(server).length) {
      throw new Error(`找不到服务器配配置, 请使用 ${colors.yellow('x3 config --add-server <server>')} 添加服务器配置`);
    }

    if (this.useServer) {
      if (server.hasOwnProperty(this.useServer)) {
        defaultServer = this.useServer;
      } else {
        throw new Error(
          `找不到指定服务器配置 ${colors.blue(this.useServer)}, 请使用 ${colors.yellow(
            'x3 config --list-server',
          )} 查看当前服务器配置`,
        );
      }
    }
    if (!defaultServer) {
      defaultServer = Object.keys(server)[0];
      log.warn(`${colors.yellow('找不到默认配置，将默认使用第一个服务器配置发布项目')}`);
    }
    log.info(`当前使用服务器配置 ${colors.blue(defaultServer)}`);
    this.defaultServer = server[defaultServer];
  }

  async checkSSH() {
    const {serverHost, sshPort, serverName, serverPassword} = this.defaultServer;
    await ssh.connect({
      host: serverHost,
      username: serverName,
      password: serverPassword,
      port: sshPort,
    });
    if (ssh.isConnected()) {
      log.verbose(`ssh 连接成功，登录用户 ${serverName}`);
    } else {
      throw new Error('ssh 连接失败');
    }
  }

  async getProjectInfo() {
    const workspaceConfigPath = path.join(process.cwd(), WORKSPACE_CONFIG_NAME);
    let projectList = [];
    // 是否使用本地工作空间配置
    if (fsExtra.pathExistsSync(workspaceConfigPath)) {
      projectList = require(workspaceConfigPath);
    } else {
      projectList = await getProjectList();
    }
    if (!projectList || projectList.length <= 0) {
      throw new Error('不存在线上项目列表');
    }
    this.projectList = projectList;

    // 判断是否指定应用名
    const packageDir = await pkgDir(process.cwd());
    if (!this.application) {
      const packagePath = path.resolve(packageDir, 'package.json');
      this.application = require(packagePath).name;
    }

    const project = this.projectList.find(project => project.packageName === this.application || project.alias === this.application);
    if (!project) {
      log.verbose('', this.projectList);
      throw new Error(`配置不存在 ${this.application} 项目配置`);
    }

    let selectProject = project;
    // 是否多项目打包
    if (Array.isArray(project.children) && project.children.length) {
      log.verbose('project.children', project.children);

      selectProject = (
        await inquirer.prompt({
          name: 'project',
          type: 'list',
          choices: project.children.map(item => ({name: item.name, value: item})),
          message: '请选择打包项目',
        })
      ).project;
    }

    log.verbose('selectProject', selectProject);
    const outputPath = path.join(packageDir, selectProject.outputPath);
    const {serverRootPath} = this.defaultServer;
    const normalize = require('normalize-path');
    const serverPath = normalize(path.join(serverRootPath, selectProject.serverRelativePath));
    log.verbose('serverPath', serverPath);

    return {
      ...selectProject,
      packageDir,
      outputPath,
      serverPath,
    };
  }

  async execCommand() {
    await this.publishProject();
  }

  async publishProject() {
    if (!this.skipBuild) {
      await this.buildProject();
    }
    await this.zipProject();
    await this.uploadServer();
    await this.clearZipFile();
  }

  async buildProject() {
    const buildCommandList = this.projectInfo.buildCommand;
    if (!buildCommandList && buildCommand.length <= 0) {
      throw new Error('不存在打包命令，请检查线上配置');
    }

    let buildCommand = this.projectInfo.buildCommand[0];
    if (buildCommandList.length >= 2) {
      buildCommand = (
        await inquirer.prompt({
          name: 'buildCommand',
          type: 'list',
          choices: this.getBuildCommandChoices(),
          message: '请选择打包命令',
        })
      ).buildCommand;
    }
    log.verbose('buildCommand', buildCommand);
    log.info('开始打包项目');
    await execCommandAsync(buildCommand);
  }

  async zipProject() {
    const distPath = this.projectInfo.outputPath;
    if (!fsExtra.pathExistsSync(distPath)) {
      throw new Error(`生成 tar 文件失败，不存在 dist 目录: ${distPath}`);
    }

    const tarName = this.projectInfo.packageName.replace(/\//g, '-') + '.tar';
    const streamFilePath = path.join(distPath, tarName);
    const output = fs.createWriteStream(streamFilePath);
    this.tarName = tarName;
    this.streamPath = streamFilePath;
    log.verbose('streamFilePath', streamFilePath);

    const archive = archiver('tar', {
      store: true,
      zlib: {level: 9},
    });
    archive.pipe(output);

    const spinner = ora('开始压缩文件').start();
    try {
      await archive.glob(`!${tarName}`, {cwd: distPath}).finalize();
      await sleep();
      spinner.succeed('压缩成功');
    } catch (error) {
      spinner.fail('压缩失败');
      throw error;
    }
  }

  async uploadServer() {
    const {serverPath, includeProject} = this.projectInfo;
    const serverFilePath = path.join(serverPath, this.tarName);
    log.verbose('serverFilePath', serverFilePath);

    const spinner = ora('备份服务器文件').start();
    const backupName = `backup-${new Date().getTime()}.tar`;
    if (!this.skipBackup) {
      // 先删除旧的备份文件
      await this.execSSHCommand(`rm -rf backup-*.tar`, serverPath);
      try {
        // 项目目录内包含项目模式，使用特殊的备份逻辑
        if (includeProject) {
          // 备份当前目录下所有非文件夹文件
          // ls -p | grep -v /$ | xargs tar -cvf backup.tar
          // 压缩包追加文件
          // tar -rvf backup.tar file.txt
          const {zipCommand} = this.projectInfo;
          for (const str of zipCommand) {
            const matchStr = matchStrObj(str, {backupName});
            log.verbose(matchStr);
            await this.execSSHCommand(matchStr, serverPath);
          }
        } else {
          await this.execSSHCommand(`tar -cvf ${backupName} ./*`, serverPath);
        }
      } catch (error) {
        throw new Error(
          `\n备份服务器文件失败\nmessage: ${error.message}\ntip: 可能服务器目录不存在文件请检查，可能你需要使用 --skip-backup 选项`,
        );
      }
    }
    spinner.succeed(this.skipBackup ? '跳过服务器文件备份' : '备份服务器文件成功').start('上传压缩文件');

    // 检测 ssh 连接是否超时，超时重新连接
    if (!ssh.isConnected()) {
      await this.checkSSH();
    }
    await ssh.putFile(this.streamPath, serverFilePath, await ssh.requestSFTP());
    spinner.succeed('压缩文件上传成功').start('开始删除服务器文件');

    // 项目目录内包含项目模式，使用特殊的删除逻辑
    if (includeProject) {
      const {removeCommand} = this.projectInfo;
      for (const str of removeCommand) {
        const matchStr = matchStrObj(str, {backupName, tarName: this.tarName});
        log.verbose(matchStr);
        await this.execSSHCommand(matchStr, serverPath);
      }
    } else {
      // 删除全部文件，保留 1.txt 和 2.txt
      // find * | grep -v '\(1.txt\|2.txt\)' | xargs rm
      const removeCommand = `find * | grep -v '\\(${this.tarName}\\|${backupName}\\)' | xargs rm -rf`;
      await this.execSSHCommand(removeCommand, serverPath);
    }
    spinner.succeed('删除服务器文件成功').start('解压服务器文件');

    await this.execSSHCommand(`tar -xvf ${this.tarName}`, serverPath);
    spinner.succeed('解压服务器文件成功');
  }

  async clearZipFile() {
    fs.unlinkSync(this.streamPath);
  }

  async execSSHCommand(command, cwd) {
    if (!cwd) {
      throw new Error('必须指定 cwd');
    }
    const response = await ssh.execCommand(command, {cwd});
    if (response.stderr) {
      throw new Error(response.stderr);
    }
    return response;
  }

  getBuildCommandChoices() {
    return this.projectInfo.buildCommand.map(command => ({
      value: command,
      name: command,
    }));
  }
}

function publish(argv) {
  return new PublishCommand(argv);
}

module.exports = publish;
module.exports.PublishCommand = PublishCommand;
